﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Devmil.MVVM.Dependencies;

namespace Devmil.MVVM.Notification
{
    public static class INotifyPropertyChangedExtension
    {
        /// <summary>
        /// Sets the value of the given Field reference and fires NotifyPropertyChanged events for all Properties that used this field
        /// to get their data.
        /// </summary>
        /// <typeparam name="T">root object type</typeparam>
        /// <typeparam name="R">property type</typeparam>
        /// <param name="object">root object</param>
        /// <param name="propertyExpression">expression targeting the property</param>
        /// <param name="field">the field reference to set</param>
        /// <param name="value">value to set</param>
        /// <returns>true if the value has been set, otherwise false (if the value hasn't been changed</returns>
        public static bool SetPropertyValue<T, R>(this T @object, Expression<Func<T, R>> propertyExpression, ref R field, R value)
            where T : INotifyPropertyChanged
        {
            R old = field;
            if ((old == null && value == null) || (old != null && value != null && old.Equals(value)))
                return false;
            field = value;
            @object.HandlePropertyValueChanged(propertyExpression);
            return true;
        }

        /// <summary>
        /// Handles Property changes by fireing the PropertyChanged event for this and all dependent properties
        /// </summary>
        /// <typeparam name="T">root object type</typeparam>
        /// <typeparam name="R">property type</typeparam>
        /// <param name="object">root object</param>
        /// <param name="propertyExpression">expression targeting the property</param>
        public static void HandlePropertyValueChanged<T, R>(this T @object, Expression<Func<T, R>> propertyExpression)
            where T : INotifyPropertyChanged
        {
            string propertyName = @object.GetMemberName(propertyExpression);
            @object.HandlePropertyValueChanged<T, R>(propertyName);
        }

        /// <summary>
        /// Handles Property changes by fireing the PropertyChanged event for this and all dependent properties
        /// </summary>
        /// <typeparam name="T">root object type</typeparam>
        /// <typeparam name="R">property type</typeparam>
        /// <param name="object">root object</param>
        /// <param name="propertyName">property name</param>
        public static void HandlePropertyValueChanged<T, R>(this T @object, string propertyName)
            where T : INotifyPropertyChanged
        {
            NotifyPropertyChangedHandler.OnNotifyPropertyChanged(@object, propertyName);
            @object.PropagateDependentChanges<T>(propertyName);
        }

        /// <summary>
        /// Propagates the PropertyChanged event to all dependent objects and fires the appropiate PropertyChanged event
        /// </summary>
        /// <typeparam name="T">root object type</typeparam>
        /// <typeparam name="R">property type</typeparam>
        /// <param name="object">root object</param>
        /// <param name="propertyExpression">expression targeting the property</param>
        public static void PropagateDependentChanges<T, R>(this T @object, Expression<Func<T, R>> propertyExpression)
            where T : INotifyPropertyChanged
        {
            @object.PropagateDependentChanges<T>(@object.GetMemberName<T, R>(propertyExpression));
        }

        /// <summary>
        /// Propagates the PropertyChanged event to all dependent objects and fires the appropiate PropertyChanged event
        /// </summary>
        /// <typeparam name="T">root object type</typeparam>
        /// <param name="object">root object</param>
        /// <param name="propertyName">name of the property</param>
        public static void PropagateDependentChanges<T>(this T @object, string propertyName)
            where T : INotifyPropertyChanged
        {
            foreach (var change in NotifyDependencyCache.GetDependentPropertyChanges(@object, propertyName))
                if (change.Key != (INotifyPropertyChanged)@object || change.Value != propertyName)
                    NotifyPropertyChangedHandler.OnNotifyPropertyChanged((INotifyPropertyChanged)change.Key, change.Value);
        }

        /// <summary>
        /// Returns the name of the member specified by the memberExpression
        /// </summary>
        /// <typeparam name="T">type of the object implementing INotifyPropertyChanged</typeparam>
        /// <typeparam name="R">result type of the member</typeparam>
        /// <param name="object">the object implementing INotifyPropertyChanged</param>
        /// <param name="memberExpression">expression for the member</param>
        /// <returns></returns>
        public static string GetMemberName<T, R>(this T @object, Expression<Func<T, R>> memberExpression)
            where T : INotifyPropertyChanged
        {
            return NotifyPropertyChangedHandler.GetMemberName(memberExpression);
        }

        /// <summary>
        /// Returns a track object to track down the call stack for getting data
        /// </summary>
        /// <typeparam name="T">type of the object implementing INotifyPropertyChanged</typeparam>
        /// <typeparam name="R">result type of the property</typeparam>
        /// <param name="object">the object implementing INotifyPropertyChanged</param>
        /// <param name="propertyExpression">expression for the property</param>
        /// <returns></returns>
        public static NotifyDependencyTracker Track<T, R>(this T @object, Expression<Func<T, R>> propertyExpression)
            where T : INotifyPropertyChanged
        {
            return NotifyDependencyTracker.Track(@object, propertyExpression);
        }

        /// <summary>
        /// Registers a ObservableCollection held by an instance property of @object targeted by the propertyExpression and tracks changes. 
        /// Whenever the collection changes the PropertyChanged event defined in the INotifyPropertyChanged interface gets invoked.
        /// </summary>
        /// <typeparam name="T">type of the root object</typeparam>
        /// <typeparam name="R">type of the elements inside the ObservableCollection</typeparam>
        /// <param name="object">root object</param>
        /// <param name="propertyExpression">expression targetting the property of the ObservableCollection</param>
        public static void RegisterCollection<T, R>(this T @object, Expression<Func<T, ObservableCollection<R>>> propertyExpression)
            where T : INotifyPropertyChanged
        {
            string propertyName = NotifyPropertyChangedHandler.GetMemberName(propertyExpression);

#if WP7
            PropertyInfo info = @object.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
#else
            PropertyInfo info = @object.GetType().GetTypeInfo().DeclaredProperties.FirstOrDefault(p => p.Name == propertyName);
#endif
            INotifyCollectionChanged collection = (INotifyCollectionChanged)info.GetValue(@object, null);
            if (collection == null)
                throw new Exception("Can't attach to a <null> collection!");
            collection.CollectionChanged += (s, e) =>
                {
                    NotifyPropertyChangedHandler.OnNotifyPropertyChanged(@object, propertyName);
                };
        }

        /// <summary>
        /// Initializes a collection in an instance property targeted by the given propertyExpression with a new instance of an ObservableCollection.
        /// Changes inside the Collection will lead to an invokation of the PropertyChanged event defined in the INotifyPropertyChanged event.
        /// </summary>
        /// <typeparam name="T">type of the root object</typeparam>
        /// <typeparam name="R">type of the elements inside the ObservableCollection</typeparam>
        /// <param name="object">root object</param>
        /// <param name="propertyExpression">expression targetting the property of the ObservableCollection</param>
        public static void InitCollection<T, R>(this T @object, Expression<Func<T, ObservableCollection<R>>> propertyExpression)
            where T : INotifyPropertyChanged
        {
#if WP7
            PropertyInfo info = @object.GetType().GetProperty(NotifyPropertyChangedHandler.GetMemberName(propertyExpression), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
#else
            PropertyInfo info = @object.GetType().GetTypeInfo().DeclaredProperties.FirstOrDefault(p => p.Name == NotifyPropertyChangedHandler.GetMemberName(propertyExpression));
#endif

            info.SetValue(@object, new ObservableCollection<R>(), null);
            @object.RegisterCollection(propertyExpression);
        }
    }
}
